
// == External Modules ====================================================================

#include "ClientMain.hpp"
#include "ImageStuff.hpp"
using namespace std;

// ========================================================================================

// Don't Ask Me For IP And Port Num When I'm Debugging The Program!
//#define CONNECT_IMMEDIATELY

struct Line {

	// Helper Structure

	int visible;

	int pointA[2];
	int pointB[2];

	float color[3];
};

// == Global Data =========================================================================

static Button2D * opponent[BOARD_ROWS * BOARD_COLS];
static Button2D * player[BOARD_ROWS * BOARD_COLS];
static Button2D * buttons[3];

static Line ScoreLine;
static int mouse[2];
static bool menu = true;
static bool needWindowReshape = false;

// Each Cell Has 100 x 100 Pixels, Plus 250 Pixels To The Main Menu.
static int WindowWidth  = (100 * BOARD_COLS);
static int WindowHeight = (100 * BOARD_ROWS) + 250;

static SocketClient * Client;
static ServerMessage ServerReply;
static ClientMessage ClientReply;

static char Symbol;

// ========================================================================================

inline void MessageWin()
{
	CLEAR_CONSOLE();
	std::cout << "Victory" << std::endl;
	std::cout << "YOU WIN!" << std::endl;
}

inline void MessageLost()
{
	CLEAR_CONSOLE();
	std::cout << "Son, I Am Disapoint..." << std::endl;
	std::cout << "You Are Terminated!" << std::endl;
}

inline void MessageDraw()
{
	CLEAR_CONSOLE();
	std::cout << "I'ts A Tie" << std::endl;
	std::cout << "Game Draw. No Winner." << std::endl;
}

ComunicationsThread::ComunicationsThread(void)
: _mtx(), _terminate_thread_flag(0), _t(0)
{
}

void ComunicationsThread::threadProc(void * param)
{
	ComunicationsThread * thread = (ComunicationsThread *)param;
	assert(thread != 0);

	LOG_MSG("Comunication Thread Started...");

	thread->_mtx.Lock();
	// This Need To Be Initialize With -1 To Prevent The Game To Begin With One X
	memset(&ClientReply, -1, sizeof(ClientMessage));
	thread->_mtx.Unlock();

	while (!thread->_terminate_thread_flag)
	{
		// Client Reply And Server Reply Are Shared Resources
		// ClientReply Are Edited By The Main Thread And By This Thread, We Can't Let The Data Sent To Server Be Corrupted
		// The Same Goes With ServerReply
		thread->_mtx.Lock();
		Client->SendBytes(reinterpret_cast<const char *>(&ClientReply), sizeof(ClientMessage));

		memset(&ClientReply, -1, sizeof(ClientMessage));

		if (!Client->ReceiveBytes(reinterpret_cast<char *>(&ServerReply), sizeof(ServerMessage)))
		{
			LOG_MSG("Server Connection Lost...");
			thread->_terminate_thread_flag = 1;
			break;
		}
		thread->_mtx.Unlock();

		// If The Player Won Or Lost, Get The Points To Draw A Line Over The Winner Symbols,
		// Show Appropriate Messages And Than Return To The Main Menu.
		if (ServerReply.playerFlags == PLAYER_WON)
		{
			MakeScoreLine(ScoreLine);
			MessageWin();
			SLEEP_MS(2000);
			menu = true;
			WindowHeight = (100 * BOARD_ROWS) + 250;
			needWindowReshape = true;
			ScoreLine.visible = 0;
			thread->_terminate_thread_flag = 1;
		}
		else if (ServerReply.playerFlags == PLAYER_LOST)
		{
			MakeScoreLine(ScoreLine);
			MessageLost();
			SLEEP_MS(2000);
			menu = true;
			WindowHeight = (100 * BOARD_ROWS) + 250;
			needWindowReshape = true;
			ScoreLine.visible = 0;
			thread->_terminate_thread_flag = 1;
		}
		else if (ServerReply.playerFlags == PLAYER_DRAW)
		{
			MessageDraw();
			SLEEP_MS(2000);
			menu = true;
			WindowHeight = (100 * BOARD_ROWS) + 250;
			needWindowReshape = true;
			thread->_terminate_thread_flag = 1;
		}

#if defined (DROP_FRAME_RATE)
		SLEEP_MS(COOL_DOWN_TIME);
#endif // DROP_FRAME_RATE
	}

	LOG_MSG("Comunication Thread Returning...");
}

void ComunicationsThread::begin()
{
	_terminate_thread_flag = 0;
	_t = new Thread(ComunicationsThread::threadProc, this);
}

ComunicationsThread::~ComunicationsThread(void)
{
	_terminate_thread_flag = 1;
	if (_t)
	{
		_t->join();
		delete _t;
	}
}

// ========================================================================================

// Thread To Handle Comunication With The Server.
ComunicationsThread * ComThread = 0;

// Mutex lock.
Mutex mtx;

// ========================================================================================

bool InitGame(void)
{
	EndGame(); // Allow Only One Initialization.

	LOG_MSG("Initialing game...");

	// Load The Sprites:
	static_image_t * pPlayerImage, * pOpponentImage;

	if (Symbol == 'O')
	{
		pPlayerImage = LoadImageFromFile("sprites/zero.tga");
		assert(pPlayerImage != NULL);

		pOpponentImage = LoadImageFromFile("sprites/cross.tga");
		assert(pOpponentImage != NULL);
	}
	else if (Symbol == 'X')
	{
		pPlayerImage = LoadImageFromFile("sprites/cross.tga");
		assert(pPlayerImage != NULL);

		pOpponentImage = LoadImageFromFile("sprites/zero.tga");
		assert(pOpponentImage != NULL);
	}
	else
	{
		return (false);
	}

	if ((pPlayerImage != NULL) && (pOpponentImage != NULL))
	{
		// Set The Image Pointers:
		player[0] = new Button2D(pPlayerImage, 0  , 96 );
		player[1] = new Button2D(pPlayerImage, 0  , 200);
		player[2] = new Button2D(pPlayerImage, 0  , 304);
		player[3] = new Button2D(pPlayerImage, 104, 96 );
		player[4] = new Button2D(pPlayerImage, 104, 200);
		player[5] = new Button2D(pPlayerImage, 104, 304);
		player[6] = new Button2D(pPlayerImage, 208, 96 );
		player[7] = new Button2D(pPlayerImage, 208, 200);
		player[8] = new Button2D(pPlayerImage, 208, 304);

		opponent[0] = new Button2D(pOpponentImage, 0  , 96 );
		opponent[1] = new Button2D(pOpponentImage, 0  , 200);
		opponent[2] = new Button2D(pOpponentImage, 0  , 304);
		opponent[3] = new Button2D(pOpponentImage, 104, 96 );
		opponent[4] = new Button2D(pOpponentImage, 104, 200);
		opponent[5] = new Button2D(pOpponentImage, 104, 304);
		opponent[6] = new Button2D(pOpponentImage, 208, 96 );
		opponent[7] = new Button2D(pOpponentImage, 208, 200);
		opponent[8] = new Button2D(pOpponentImage, 208, 304);

		mtx.Lock();

		// '*' Means No Piece
		memset(ServerReply.gameBoard, '*', BOARD_ROWS * BOARD_COLS);

		// Clear Out Net Data
		ServerReply.playerFlags = 0;
		memset(&ClientReply, -1, sizeof(ClientMessage));

		mtx.Unlock();

        menu = false;
		WindowHeight = (100 * BOARD_ROWS) + 5;
		ReshapeWindow(0,0);

		glutPostRedisplay();

		CLEAR_CONSOLE();
		return (true);
	}

	return (false);
}

void EndGame(void)
{
	unsigned int i;

	if (*player != 0)
	{
		i = (BOARD_ROWS * BOARD_COLS); // Free Player Sprites.

		(*player)->FreeMemory();

		while (i--)
		{
			delete player[i];
			player[i] = 0;
		}
	}

	if (*opponent != 0)
	{
		i = (BOARD_ROWS * BOARD_COLS); // Free The Opponent Sprites.

		(*opponent)->FreeMemory();

		while (i--)
		{
			delete opponent[i];
			opponent[i] = 0;
		}
	}
}

bool Connect(void)
{
	int port;
	std::string host;

	LOG_MSG("Starting connection...");

#if defined (CONNECT_IMMEDIATELY)
	port = 1337;
	host = "localhost";
#else
	cout << '\n';

	// Try To Connect...
	cout << "Type The IP Adress That You Want To Connect: ";
	cin >> host;
	cout << '\n';
	cout << "Type The Port Number That You Want To Use (T4 Server Default = 1337): ";
	cin >> port;
	cout << '\n';

	int cnt = 15; // Make A Simple Progress Bar In The Console:
	printf("Trying To Connect...");
	while (cnt--)
	{
		fputc('.', stdout);
		SLEEP_MS(200);
	}

	cout << '\n';
	cout << '\n';
	cout << "Waiting For Server To Start Game..." << endl;
#endif // CONNECT_IMMEDIATELY

	CloseConnection(); // Only One Connection At A Time.

	try {

		Client = new SocketClient(host, port);
	}
	catch (const char * e) {

		LOG_MSG("Failed To Connect To Host " << host.c_str() << " At Port N: " << port);
		LOG_ERROR(e);
		return (false);
	}
	catch (std::string e) {

		LOG_MSG("Failed To Connect To Host " << host.c_str() << " At Port N: " << port);
		LOG_ERROR(e.c_str());
		return (false);
	}

	char msg[ASCII_MSG_MAX_LEN];
	strcpy(msg, "T4Client-Connecting");

	// Send First Message To Server And Wait For A Reply:

	if (!Client->SendBytes(msg, ASCII_MSG_MAX_LEN))
	{
		LOG_ERROR("Failed To Send First Data Package To Server.");
		return (false);
	}

	if (!Client->ReceiveBytes(msg, ASCII_MSG_MAX_LEN))
	{
		LOG_ERROR("The Server Didn't Reply !");
		return (false);
	}

	if (strcmp(msg, "T4Server-Responding") != 0)
	{
		LOG_ERROR("Invalid Data Received !");
		return (false);
	}

	if (!Client->ReceiveBytes(msg, ASCII_MSG_MAX_LEN))
	{
		LOG_ERROR("The Server Didn't Reply !");
		return (false);
	}

	// Get The Player Symbol:
	if (sscanf(msg, "Player Symbol = %c", &Symbol) != 1)
	{
		LOG_ERROR("Did Not Receive The Player Symbol.");
		return (false);
	}

	// Now Get The Initial Game Flags:
	if (!Client->ReceiveBytes(reinterpret_cast<char *>(&ServerReply.playerFlags), sizeof(int)))
	{
		LOG_ERROR("The Server Didn't Reply !");
		return (false);
	}

	// If At This Point Player Status Is Not Equal To PLAYER_IN_GAME, An Error Ocurred.
	if (ServerReply.playerFlags != PLAYER_IN_GAME)
	{
		LOG_ERROR("Server Failed To Start Game.");
		return (false);
	}

	ComThread = new ComunicationsThread();
	ComThread->begin();

	LOG_MSG("Connection established");

	return (true);
}

void CloseConnection(void)
{
	if (ComThread)
	{
		delete ComThread;
		ComThread = 0;
	}

	if (Client)
	{
		Client->Close();
		delete Client;
		Client = 0;
	}
}

void InitOpenGL(void)
{
	LOG_MSG("Initializing OpenGL");

	glMatrixMode(GL_PROJECTION);
    glLoadIdentity();

	// Set 2D Projection:
	glOrtho(0.0, static_cast<double>(WindowWidth), static_cast<double>(WindowHeight), 0.0, -1.0, 1.0);

	glMatrixMode(GL_MODELVIEW);
	glLoadIdentity();

	glLineWidth(8.0f);
	glClearColor(0.0f, 0.0f, 0.0f, 0.0f);

	// Prepare OpenGL To Draw Sprites:
	glDisable(GL_TEXTURE_2D);
	glEnable(GL_COLOR_MATERIAL);

	glEnable(GL_BLEND);
	glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
	glEnable(GL_ALPHA_TEST);
	glAlphaFunc(GL_GREATER, 0);

	static_image_t * sprite;

	// Loading Some Resources:
	sprite = LoadImageFromFile("sprites/startGame.tga");
	assert(sprite != NULL);
	buttons[0] = new Button2D(sprite, 30, 370);

	sprite = LoadImageFromFile("sprites/exitGame.tga");
	assert(sprite != NULL);
	buttons[1] = new Button2D(sprite, 30, 500);

	sprite = LoadImageFromFile("sprites/titleScreen.tga");
	assert(sprite != NULL);
	buttons[2] = new Button2D(sprite, 10, 230);

	LOG_MSG("OpenGL ready, sprites loaded successfully");
}

void Terminate(void)
{
	LOG_MSG("T4 Game Client Terminated...");

	CloseConnection();
	EndGame();

	buttons[0]->FreeMemory();
	buttons[1]->FreeMemory();
	buttons[2]->FreeMemory();
}

void DisplayCallback(void)
{
	if (needWindowReshape)
	{
		ReshapeWindow(0,0);
		needWindowReshape = false;
	}

	glClear(GL_COLOR_BUFFER_BIT);
	glLoadIdentity();

	glColor3f(0.7f, 0.7f, 0.7f);
	unsigned int i, j;

	if (!menu)
	{
		// Draw The Correct Sprite:
		for (i = 0; i < BOARD_ROWS; i++)
		{
			for (j = 0; j < BOARD_COLS; j++)
			{
				if (ServerReply.gameBoard[i][j] == '*')
				{
					continue; // Empty Cell.
				}
				else if (ServerReply.gameBoard[i][j] == Symbol)
				{
					player[j + i * BOARD_COLS]->DrawButton();
				}
				else
				{
					opponent[j + i * BOARD_COLS]->DrawButton();
				}
			}
		}

		// Horizontal Lines:
		for (j = 1; j < BOARD_ROWS; j++)
		{
			glBegin(GL_LINE_STRIP);
			glVertex2i(0, j * 100);
			glVertex2i(WindowWidth, j * 100);
			glEnd();
		}

		// Vertical Lines:
		for (j = 1; j < BOARD_COLS; j++)
		{
			glBegin(GL_LINE_STRIP);
			glVertex2i(j * 100, 0);
			glVertex2i(j * 100, WindowHeight+5);
			glEnd();
		}

		// Line Indicating Which Player Won:
		if (ScoreLine.visible)
		{
			glColor3fv(ScoreLine.color);

			glBegin(GL_LINE_STRIP);
			glVertex2iv(ScoreLine.pointA);
			glVertex2iv(ScoreLine.pointB);
			glEnd();
		}

		static_image_t * cursorImage = player[0]->returnImage(); // Get Any Player Image...

		// Draw The Sprite At Mouse X - Half The Image Width And At Mouse Y + Half The Image Height.
		DrawImage(mouse[0] - (cursorImage->width >> 1), mouse[1] + (cursorImage->height >> 1), cursorImage);
	}
	else
	{
		buttons[0]->DrawButton();
		buttons[1]->DrawButton();
		buttons[2]->DrawButton();
	}

#if defined (DROP_FRAME_RATE)
	SLEEP_MS(COOL_DOWN_TIME);
#endif // DROP_FRAME_RATE

	glutSwapBuffers();
}

void MakeScoreLine(Line & line)
{
	/*
		This Function Computes The Points For A Line That Is Drawn Over The Player Pieces
		If He Wins. The Process Is Similar To The Player Status Verification Done By The Server
		However, The Win, Lose Or Draw Condition Of The Player Is Not Computed Here, But In The Remote Server.
	*/
	Button2D * bt1, * bt2;
	static_image_t * img1, * img2;
	const char SymbolTable[] = {'X', 'O'};

	for (int j = 0; j < 2; j++)
	{
		for (int i = 0; i < 3; i++)
		{
			// Vertical Lines
			if ((ServerReply.gameBoard[i][0] == SymbolTable[j]) &&
				(ServerReply.gameBoard[i][1] == SymbolTable[j]) && (ServerReply.gameBoard[i][2] == SymbolTable[j]))
			{
				bt1 = player[0 + i * BOARD_COLS];
				bt2 = player[2 + i * BOARD_COLS];
				img1 = bt1->returnImage();
				img2 = bt2->returnImage();

				line.pointA[0] = bt1->getX() + (img1->width >> 1);
				line.pointA[1] = bt1->getY() - img1->height;
				line.pointB[0] = bt2->getX() + (img2->width >> 1);
				line.pointB[1] = bt2->getY();
				line.visible = 1;

				if (j == 0) // Set The Color
				{
					line.color[0] = 0.8f; line.color[1] = 0.0f; line.color[2] = 0.0f;
				}
				else
				{
					line.color[0] = 0.0f; line.color[1] = 0.0f; line.color[2] = 0.8f;
				}
				return;
			}

			// Horizontal Lines
			if ((ServerReply.gameBoard[0][i] == SymbolTable[j]) &&
				(ServerReply.gameBoard[1][i] == SymbolTable[j]) && (ServerReply.gameBoard[2][i] == SymbolTable[j]))
			{
				bt1 = player[i + 0 * BOARD_COLS];
				bt2 = player[i + 2 * BOARD_COLS];
				img1 = bt1->returnImage();
				img2 = bt2->returnImage();

				line.pointA[0] = bt1->getX();
				line.pointA[1] = bt1->getY() - (img1->height >> 1);
				line.pointB[0] = bt2->getX() + img2->width;
				line.pointB[1] = bt2->getY() - (img2->height >> 1);
				line.visible = 1;

				if (j == 0) // Set The Color
				{
					line.color[0] = 0.8f; line.color[1] = 0.0f; line.color[2] = 0.0f;
				}
				else
				{
					line.color[0] = 0.0f; line.color[1] = 0.0f; line.color[2] = 0.8f;
				}
				return;
			}
		}

		// Diagonal 1
		if ((ServerReply.gameBoard[0][0] == SymbolTable[j]) &&
			(ServerReply.gameBoard[1][1] == SymbolTable[j]) && (ServerReply.gameBoard[2][2] == SymbolTable[j]))
		{
			bt1 = player[0];
			bt2 = player[8];
			img1 = bt1->returnImage();
			img2 = bt2->returnImage();

			line.pointA[0] = bt1->getX();
			line.pointA[1] = bt1->getY() - img1->height;
			line.pointB[0] = bt2->getX() + img2->width;
			line.pointB[1] = bt2->getY();
			line.visible = 1;

			if (j == 0) // Set The Color
			{
				line.color[0] = 0.8f; line.color[1] = 0.0f; line.color[2] = 0.0f;
			}
			else
			{
				line.color[0] = 0.0f; line.color[1] = 0.0f; line.color[2] = 0.8f;
			}
			return;
		}

		// Diagonal 2
		if ((ServerReply.gameBoard[0][2] == SymbolTable[j]) &&
			(ServerReply.gameBoard[1][1] == SymbolTable[j]) && (ServerReply.gameBoard[2][0] == SymbolTable[j]))
		{
			bt1 = player[6];
			bt2 = player[2];
			img1 = bt1->returnImage();
			img2 = bt2->returnImage();

			line.pointA[0] = bt1->getX() + img1->width;
			line.pointA[1] = bt1->getY() - img1->height;
			line.pointB[0] = bt2->getX();
			line.pointB[1] = bt2->getY();
			line.visible = 1;

			if (j == 0) // Set The Color
			{
				line.color[0] = 0.8f; line.color[1] = 0.0f; line.color[2] = 0.0f;
			}
			else
			{
				line.color[0] = 0.0f; line.color[1] = 0.0f; line.color[2] = 0.8f;
			}
			return;
		}
	}
}

void ReshapeWindow(int w, int h)
{
	// Block Window Reshape:
	glutReshapeWindow(WindowWidth, WindowHeight);

	// Reset The Viewport:
	glViewport(0, 0, WindowWidth, WindowHeight);

	glMatrixMode(GL_PROJECTION);
    glLoadIdentity();

	// Set 2D Projection:
	glOrtho(0.0, static_cast<double>(WindowWidth), static_cast<double>(WindowHeight), 0.0, -1.0, 1.0);

	glMatrixMode(GL_MODELVIEW);
	glLoadIdentity();
}

void MouseMotionCallback(int x, int y)
{
	mouse[0] = x;
	mouse[1] = y;
}

void MouseCallback(int button, int state, int x, int y)
{
	if ((button == GLUT_LEFT_BUTTON) && (state == GLUT_DOWN))
	{
		if (menu)
		{
			if (buttons[0]->testColision(x, y))
			{
				// Start The Game
				if (Connect())
				{
					if (!InitGame())
					{
						LOG_MSG("An Error Ocurred, Please Try Again...");
					}
				}
				else
				{
					LOG_MSG("An Error Ocurred, Please Try Again...");
				}
			}
			else if (buttons[1]->testColision(x, y))
			{
				// Leave The Game
				exit(0);
			}
		}
		else
		{
			for (int i = 0; i < BOARD_ROWS; i++)
			{
				for (int j = 0; j < BOARD_COLS; j++)
				{
					if ((ServerReply.gameBoard[i][j] == '*') && player[j + i * BOARD_COLS]->testColision(x, y))
					{
						mtx.Lock();
						ClientReply.updatedRow = i;
						ClientReply.updatedCol = j;
						mtx.Unlock();
						break;
					}
				}
			}
		}

		glutPostRedisplay();
	}
}

void KeyboardCallback(unsigned char key, int x, int y)
{
	switch (key)
	{
	case 27: // ESCAPE Key
		exit(0);
	}
}

// == int main(void) - Application Entry Point  ===========================================

int main(int argc, char ** argv)
{
	LOG_MSG("T4 Game Client Up And Running...");

	try {

		// Set Up Glut & OpenGL
		glutInit(&argc, argv);
		glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA);
		glutInitWindowSize(WindowWidth, WindowHeight);
		glutCreateWindow("[T4] - Tic Tac Toe TCP");
		atexit(Terminate);

		InitOpenGL();

		// Window
		glutReshapeFunc(ReshapeWindow);

		// Display
		glutDisplayFunc(DisplayCallback);
		glutIdleFunc(DisplayCallback);

		// Mouse / Keyboard
		glutKeyboardFunc(KeyboardCallback);
		glutPassiveMotionFunc(MouseMotionCallback);
		glutMouseFunc(MouseCallback);

		LOG_MSG("Entering main loop...");

		glutMainLoop();
	}
	catch (...) {

		LOG_FATAL_ERROR("Unhandled Exception.");
	}

	// Never Gets Here...
	return (0);
}
